Air Monitoring Data
API documentation for the air monitoring data published by Martinez Refining Company: how to call each endpoint, and how to download the same data by hand.
Four data sets are published, for four different purposes:
| Data set | What it is | Manual download | API |
|---|---|---|---|
| Fenceline Monitoring Data | Five-minute average pollutant concentrations, one record per instrument, parameter and averaging period, with detection limits, quality codes and instrument signal. | CSV, XML | CSV, JSON |
| GLM Data | Five-minute average concentrations from the four ground level monitors operated under Air District Rules 9-1 and 9-2. Hydrogen sulfide at all four, sulfur dioxide at Ace Hardware. | CSV, XML | CSV, JSON |
| Monitoring Locations | Where each analyzer, reflector, light source, met station and ground level monitor sits, and what it reports. Fixed positions, so there is no date range. | CSV, GeoJSON, shapefile | CSV, JSON, GeoJSON |
| Current Readings | The single latest reading for every monitoring location and property, as shown on the public dashboard. Includes weather properties. | None | JSON |
Fenceline Monitoring Data
- Manual download: CSV and XML
- API: CSV and JSON
- Averaging period: 5 minutes
- Time zone: Pacific Standard Time, year-round
- Access: Public, with no login or API key required for the API
API
- Endpoint:
GET /api-fenceline-monitoring-data.php - Access: Public, with no login or API key required
- Formats: JSON (default) and CSV
- Rate limit: 120 requests/hour per IP (HTTP 429 when exceeded)
- Per-response range: 31 days for one location, divided by the number of locations requested, so about 3 days for all ten
The API serves real-time data as well as historical. It will return the most recent completed averaging period, which the manual download will not.
Retrieving a long period
Nothing caps how much of the record you may retrieve, only how much of it arrives in one response. You do not need to work out window sizes, and asking for too much is not an error. Ask for the period you want; if it is larger than one response can carry, you get the start of it and the response tells you where to continue.
Every response reports the range it served and what to do next:
| JSON field | CSV response header | Meaning |
|---|---|---|
startEpoch / endEpoch |
X-Start-Epoch / X-End-Epoch |
The range this response covers, which may be narrower than the one requested. |
nextStartEpoch | X-Next-Start-Epoch |
Use this as start-epoch on your next request. |
hasMore | X-Has-More |
true while data remains after this response. Loop until it
reads false. |
truncated | X-Truncated |
true when the requested range was larger than one response
carries. requestedEndEpoch then reports the end the request
asked for. |
maxRangeSeconds | X-Max-Range-Seconds |
The largest range one response will carry for the number of locations you requested. |
earliestEpoch | None | The earliest reading available. A start-epoch before this
moves forward to it, and the response says so in a note field
instead of returning empty records for years the system did not run. |
Retrieving everything takes no arithmetic. Ask for start-epoch=0 and
keep going until hasMore reads false. You do not need to know
when the record begins, how large a window to ask for, or how many requests it will
take.
Example: Python
Downloads the complete record for every location and writes it to one CSV. Standard library only, no packages to install.
import csv, json, time, urllib.error, urllib.parse, urllib.request
BASE = "https://martinez.argos-scientific.com/api-fenceline-monitoring-data.php"
def get_page(url):
"""Fetch one page, waiting out the rate limit instead of failing."""
while True:
try:
with urllib.request.urlopen(url) as response:
return json.load(response)
except urllib.error.HTTPError as error:
if error.code != 429:
raise
time.sleep(60)
def fetch_all(start_epoch=0, **params):
"""Yield every record from start_epoch onward, one response at a time."""
while True:
query = urllib.parse.urlencode({**params, "start-epoch": start_epoch}, doseq=True)
page = get_page(f"{BASE}?{query}")
yield from page["records"]
if not page["hasMore"]:
break
start_epoch = page["nextStartEpoch"]
with open("fenceline.csv", "w", newline="", encoding="utf-8") as f:
writer = None
for record in fetch_all():
if writer is None:
writer = csv.DictWriter(f, fieldnames=list(record))
writer.writeheader()
writer.writerow(record)
To restrict it to one location, or to start somewhere other than the beginning, pass
the same parameters the endpoint takes:
fetch_all(**{"site-keys[]": ["path1"]}), or
fetch_all(start_epoch=1785240000).
Both examples pause on the rate limit and carry on. At 120 requests per hour most retrievals never reach it: a year of one location runs about 12 requests, and eight months of all ten locations about 79. A multi-year retrieval across every location will reach it, and the loop then waits and resumes on its own. Handle it even if you expect never to hit it. A rejected request otherwise looks like the end of the data, and the loop stops early on a file that looks complete.
Example: shell
The same job with curl alone. In CSV mode the continuation values arrive
as response headers, so nothing has to parse JSON. Each page after the first loses its
header row, so the result is one well-formed CSV.
BASE="https://martinez.argos-scientific.com/api-fenceline-monitoring-data.php"
START=0
: > fenceline.csv
while : ; do
STATUS=$(curl -s -D headers.txt -o page.csv -w '%{http_code}' "$BASE?format=csv&start-epoch=$START")
# Rate limited: wait, then ask for the same window again.
if [ "$STATUS" = "429" ]; then sleep 60; continue; fi
# Any other non-200 has to stop the loop loudly. Carrying on would leave a
# truncated file that looks complete.
if [ "$STATUS" != "200" ]; then echo "HTTP $STATUS" >&2; exit 1; fi
if [ -s fenceline.csv ]; then tail -n +2 page.csv >> fenceline.csv; else cat page.csv > fenceline.csv; fi
grep -qi '^x-has-more: true' headers.txt || break
START=$(grep -i '^x-next-start-epoch:' headers.txt | tr -dc '0-9')
done
Forgiving inputs
The API interprets the range parameters. How you express a request does not make it fail:
- Omit both bounds and you get the most recent hour, which suits polling.
- Give one bound and you get a full-sized window from it.
- Give a reversed range and the API reads it the way round you meant.
- A range shorter than one averaging period widens to one.
- A range larger than one response can carry is served up to the limit, with
truncatedset andnextStartEpochpointing at the rest. - A
start-epochbefore the earliest reading moves forward to it, and the response says so in anotefield.
The API still rejects two inputs: an unknown location key and an unknown format. Guessing what either one meant would be worse than saying so.
| Parameter | Required | Description |
|---|---|---|
site-keys[] | No | Repeatable. Restricts the response to the named monitoring locations.
Omit it for all of them. Valid keys: sitea, siteb,
sitec, sited, sitef,
siteg, path1, path2,
path3, path4. |
start-epochend-epoch | No | Unix timestamps in seconds, bounding the period requested. Either one works on its own, as described under Forgiving inputs above. Omit both to get the most recent hour, which suits polling for current data. |
format | No | json (default) or csv. |
The most recent hour for every location, as JSON:
GET /api-fenceline-monitoring-data.php
{
"generatedAt": "2026-07-28T20:47:00+00:00",
"sites": "Site A, Site B, Site C, Site D, Site F, Site G, Path 1, Path 2, Path 3, Path 4",
"startEpoch": 1785268020,
"endEpoch": 1785271620,
"maxRangeSeconds": 267840,
"truncated": false,
"nextStartEpoch": 1785271620,
"hasMore": false,
"earliestEpoch": 1784966400,
"records": [
{
"facility_name": "Martinez Refining Company",
"instrument_id": "path1_uv",
"instrument": "Path 1 UV-DOAS",
"parameter": "Benzene",
"date": "2026-07-28",
"time": "12:40",
"mean_concentration": 0.246609,
"units_of_measure": "ppb",
"averaging_period": 5,
"observation_count": 1,
"validity_indicator": "Y",
"error_codes": "",
"max_value": 0.246609,
"required_loq": 0.9,
"real_time_loq": 0.493218,
"signal": 77131.3,
"signal_units": "light count",
"QC_code": "20",
"final_data": "N",
"change_log": ""
}
]
}
One location, as CSV, for a specific period:
GET /api-fenceline-monitoring-data.php?site-keys[]=path1&format=csv
&start-epoch=1785240000&end-epoch=1785243600
An unrecognised site-keys[] value or an unsupported format
returns HTTP 400 with a plain-text explanation. Exceeding the rate limit returns
HTTP 429.
Manual download
The website's Download Data page serves the same fields as CSV or XML, and covers historical data only: its range stops one averaging period before the present, and a request falling entirely inside that most recent period comes back with an explanation pointing to the API. The form asks for a captcha phrase, so a script should use the API above.
One location and up to 31 days per download. For a longer period, take consecutive downloads, or use the API, which walks the whole record for you. A request for more than 31 days returns the first 31; the file name states the period the file covers.
GLM Data
- Endpoint:
GET /api-glm-data.php - Access: Public, with no login or API key required
- Output formats:
json(default) andcsv - Rate limit: 240 requests/hour per IP (HTTP 429 when exceeded)
Five-minute records from the four ground level monitors. Field meanings are in the
data dictionary.
Records carry four fewer fields than the Fenceline Monitoring Data: no
real_time_loq, signal, signal_units or
QC_code.
| Parameter | Description |
|---|---|
glm-keys[] | One or more of glm_ace, glm_mtview, glm_shellave, glm_etp. Omit for all four. |
start-epoch | Unix timestamp, start of the range. Omit both bounds for the most recent hour. |
end-epoch | Unix timestamp, end of the range. |
format | json or csv. |
Ranges are handled the same way as the Fenceline Monitoring Data API above: a
reversed range is read the way it was meant, a range longer than one response can carry
is served up to the limit with hasMore and nextStartEpoch
saying where to continue, and only an unknown monitor key or format is refused.
GET /api-glm-data.php?glm-keys[]=glm_ace&format=json
{
"generatedAt": "2026-08-27T23:52:34+00:00",
"sites": "Ace GLM",
"startEpoch": 1787788800,
"endEpoch": 1787789400,
"nextStartEpoch": 1787789400,
"hasMore": true,
"records": [
{
"facility_name": "Martinez Refining Company",
"instrument_id": "glm_ace",
"instrument": "Ace GLM",
"parameter": "Hydrogen Sulfide",
"date": "2026-08-26",
"time": "16:00",
"mean_concentration": 2.4,
"units_of_measure": "ppb",
"averaging_period": 5,
"observation_count": "",
"validity_indicator": "Y",
"error_codes": "",
"max_value": 2.4,
"required_loq": "",
"final_data": "N",
"change_log": ""
}
]
}
Manual download
The Download Data page offers the same records as CSV or XML over a chosen date range. The monitors appear in the same location picker as the fenceline sites, one location per download, stopping five minutes short of the present as that download does.
Monitoring Locations
- Manual download: CSV, GeoJSON and shapefile
- API: CSV, JSON and GeoJSON
- Coordinate system: WGS 84, decimal degrees
- Access: Public, with no login or API key required for the API
Where each monitoring instrument sits, and what it reports. There is no date range;
these are fixed installation positions. Each published point gets one record: a point
instrument, the met station and each ground level monitor have one apiece, and an open
path has two. Both ends of a path share an instrument_id and are told apart
by the instrument name.
The two open path instrument types are built differently, so the far end of a path is
not the same thing in both cases. A TDL sends its beam to a corner cube reflector and
reads the return at the analyzer, so its two points are
Open Path Analyzer and Reflector. A UV-DOAS puts a lamp at one
end and the analyzer at the other, with no reflector on the path at all, so its two
points are Open Path Analyzer and Light Source. The analyzer
end carries that name in both cases.
API
- Endpoint:
GET /api-geospatial-data.php - Access: Public, with no login or API key required
- Formats: JSON (default), CSV and GeoJSON
- Rate limit: 120 requests/hour per IP (HTTP 429 when exceeded)
| Parameter | Required | Description |
|---|---|---|
format | No | json (default), csv or
geojson. |
Manual download
The website's Download Data page carries these under Monitoring Locations, as CSV,
GeoJSON or shapefile. The shapefile download is a zip holding the .shp,
.shx, .dbf and .prj files, which have to travel
together.
Each is also a plain link, so you can bookmark it or fetch it from a script:
GET /geospatial-data.php?format=csvGET /geospatial-data.php?format=geojsonGET /geospatial-data.php?format=shapefile
Current Readings
- Endpoint:
GET /api-data.php - Access: Public, with no login or API key required
- Parameters: None
- Rate limit: 120 requests/hour per IP (HTTP 429 when exceeded)
- Output format: JSON only
This endpoint takes no parameters. Every request returns the same thing: the latest reading for every monitored location and property as of the moment of the request, covering every pollutant at every monitoring location plus every weather property at the met station. Ozone appears here; the Fenceline Monitoring Data leaves it out. There is no way to ask this endpoint for a specific location, property or date range. For those, use the Fenceline Monitoring Data API above, or the Download Data page.
GET /api-data.php
{
"generatedAt": "2026-07-28T20:45:00+00:00",
"records": [
{
"site": "path1",
"siteName": "Path 1",
"property": "ben",
"propertyName": "Benzene",
"unit": "ppb",
"timestamp": "2026-07-28T20:45:00+00:00",
"value": 0.246609,
"loq": 0.493218,
"description": "No Detection",
"online": true
},
{
"site": "met",
"siteName": "Met",
"property": "wind_speed",
"propertyName": "Wind Speed",
"unit": "mph",
"timestamp": "2026-07-28T20:45:00+00:00",
"value": 5.9,
"loq": null,
"description": null,
"online": true
}
]
}
Related resources
The website's Learning Center defines the terms and acronyms used here, explains each data status label, and lists the current Limit of Quantification for every gas and monitoring location.
The Documents page holds the Fence Line Air Monitoring Plan, the Quality Assurance Project Plan and the quarterly monitoring reports.
The Download Data page provides the Fenceline Monitoring Data as CSV or XML over a chosen date range, stopping five minutes short of the present as above, and the Monitoring Locations as CSV, GeoJSON or shapefile.